﻿//////////////////////////////////////////////////////////////////
//
// Network\NetworkGui.cs
//
// Copyright (c) 2012 Dan Pike.
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation files
// (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
using Interfaces;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Utilities;

namespace Network
{
   public abstract class NetworkGui
   {
      ////////////////////////////////////////////////////////////////////////
      //
      public class GuiObjects
      {
         public Form hostForm;
         public StatusStrip statusBar;
         public ToolStripStatusLabel statusText;
         public ToolStripStatusLabel statusNetwork;
         public ToolStripStatusLabel statusHeartbeat;
         public ToolStripMenuItem menuLogin;
         public ToolStripMenuItem menuLogout;
      }

      ////////////////////////////////////////////////////////////////////////
      //
      public class SenderChangedEventArgs : EventArgs
      {
         public string senderId;
         public bool isAlive;
         public string statusText;
         public bool isClient;
         public string planetName;
      }
      public delegate void SenderChangedHandler(object sender, SenderChangedEventArgs eventArgs);
      public event SenderChangedHandler EventSenderChanged;

      ////////////////////////////////////////////////////////////////////////
      //
      protected static Logger logger_;
      private string senderId_;
      private GuiObjects gui_;
      private IAccount account_;
      private HeartbeatStatus status_;
      private string statusText_;
      private bool transportConnected_;
      private DateTime nextBeat_;
      private Dictionary<string, HeartbeatSender> senders_;

      /// <summary>
      /// True if we are a server hosting planets, else we are a client
      /// that will connect to a planet being hosted elsewhere
      /// </summary>
      private bool isClient_;

      /// <summary>
      /// The planet that we are running/watching
      /// </summary>
      private IPlanet planet_;

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      public NetworkGui(string senderId, GuiObjects gui, bool isClient)
      {
         senderId_ = senderId;
         isClient_ = isClient;
         gui_ = gui;
         status_ = HeartbeatStatus.None;
         statusText_ = string.Empty;
         transportConnected_ = false;
         nextBeat_ = DateTime.Now;
         senders_ = new Dictionary<string, HeartbeatSender>();
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <returns></returns>
      public bool Initialize()
      {
         try
         {
            // DON'T DO THIS IN THE CONSTRUCTOR, IT HASN'T BEEN INITIALIZED UNTIL NOW
            logger_ = Program.Logger;

            // Connect to the configured transport layer
            Program.Transport.EventStateChanged += new NetworkStateChangedHandler(HandleStateChanged);
            Program.Transport.EventReceivedMessage += new ReceivedMessageHandler(HandleReceivedMessage);
            if (!Program.Transport.Initialize(senderId_))
            {
               return false;
            }

            // Link to the GUI
            gui_.menuLogin.Click += new EventHandler(loginMenu_Click);
            gui_.menuLogout.Click += new EventHandler(logoutMenu_Click);
            return true;
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      public virtual void Shutdown()
      {
         try
         {
            logger_.DebugFormat("shutting down NetworkGui");
            Logout(true);
            if (null != Program.Transport)
            {
               Program.Transport.Shutdown();
            }
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      ///
      /// </summary>
      public IAccount Account { get { return account_; } }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// The active planet, either we are running it (server) or we are
      /// watching it (client)
      /// </summary>
      public IPlanet Planet
      {
         get { return planet_; }
         set { planet_ = value; }
      }

      public string PlanetName
      {
         get
         {
            return (null != planet_) && (null != planet_.Fact) ? planet_.Fact.DisplayName : null;
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of INetwork event
      /// </summary>
      protected abstract bool HandleReceivedMessage(ReceivedMessageEventArgs eventArgs);
      private void HandleReceivedMessage(object sender, ReceivedMessageEventArgs eventArgs)
      {
         try
         {
            IHeartbeat heartbeat;
            if (null != (heartbeat = eventArgs.message as IHeartbeat))
            {
               // It's a heartbeat message. We handle these internally
               ProcessHeartbeat(heartbeat);
            }
            else
            {
               // Check to see if the client/server wants to see the message
               HandleReceivedMessage(eventArgs);
            }
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="sender"></param>
      private void NotifySenderChanged(HeartbeatSender sender)
      {
         try
         {
            if (null != EventSenderChanged)
            {
               // Remember the state that we last notified
               sender.WasAlive = sender.IsAlive;

               // Tell the client
               SenderChangedEventArgs eventArgs = new SenderChangedEventArgs();
               eventArgs.senderId = sender.SenderId;
               eventArgs.isAlive = sender.IsAlive;
               eventArgs.statusText = sender.StatusText;
               eventArgs.isClient = sender.IsClient;
               eventArgs.planetName = sender.Planet;
               EventSenderChanged(this, eventArgs);
            }
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="message"></param>
      private void ProcessHeartbeat(IHeartbeat heartbeat)
      {
         try
         {
            // Update the GUI to show when we last received our heartbeat
            SafeAsyncInvoker.BeginInvoke(gui_.statusBar, () =>
            {
               gui_.statusHeartbeat.Text = heartbeat.Timestamp.ToString("HH:mm:ss");
               gui_.statusHeartbeat.ToolTipText = heartbeat.Timestamp.ToString("dd MMM yyyy");
            });

            // Do we already know this sender?
            HeartbeatSender sender;
            if (!senders_.TryGetValue(heartbeat.SenderId, out sender))
            {
               sender = new HeartbeatSender(HeartbeatSeconds, heartbeat.SenderId);
               senders_.Add(sender.SenderId, sender);
            }
            sender.DisplayName = heartbeat.DisplayName;
            sender.IsClient = heartbeat.IsClient;

            bool needNotify = false;
            if (sender.Planet != heartbeat.Planet)
            {
               sender.Planet = heartbeat.Planet;
               needNotify = true;
            }
            if (sender.StatusText != heartbeat.StatusText)
            {
               sender.StatusText = heartbeat.StatusText;
               needNotify = true;
            }
            switch (heartbeat.Status)
            {
               case HeartbeatStatus.Active:
                  if (sender.SetLastAlive(heartbeat.Timestamp))
                  {
                     needNotify = true;
                  }
                  break;

               default:
               case HeartbeatStatus.Inactive:
               case HeartbeatStatus.Stale:
                  if (sender.IsAlive)
                  {
                     sender.IsAlive = false;

                     // Cancel this game session (client only)
                     if ((null != account_) && !string.IsNullOrEmpty(account_.SessionId))
                     {
                        logger_.WarnFormat("heartbeat failed, cancelling game session");
                        account_.SessionId = null;
                     }
                     needNotify = true;
                  }
                  break;
            }
            if (needNotify)
            {
               NotifySenderChanged(sender);
            }
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="now"></param>
      public virtual void Heartbeat(DateTime now)
      {
         try
         {
            // Check all of the senders that we know about to see if they
            // have gone away
            senders_.Values.OfType<HeartbeatSender>().ToList()
               .ForEach(sender =>
               {
                  if (sender.WasAlive && !sender.IsAlive)
                  {
                     sender.StatusText = "heartbeat expired";
                     sender.Planet = string.Empty;
                     NotifySenderChanged(sender);
                  }
               });

            // Then 
            if ((nextBeat_ > now) || !transportConnected_)
            {
               // Too soon
               return;
            }

            // Construct a heartbeat message
            IHeartbeat hb = Program.Loader.Create<IHeartbeat>();
            hb.DisplayName = Application.ProductName;
            hb.Status = status_;
            hb.StatusText = statusText_;
            hb.Planet = PlanetName;
            hb.IsClient = isClient_;

            // Send it to all connected listeners
            Program.Transport.Broadcast<IHeartbeat>(hb);

            // Send two heartbeats per period, to avoid hazard
            nextBeat_ = now + new TimeSpan(0, 0, 0, 0, Program.Settings.HeartbeatSeconds * 500);
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      void loginMenu_Click(object sender, EventArgs e)
      {
         try
         {
            Login(false);
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      void logoutMenu_Click(object sender, EventArgs e)
      {
         try
         {
            Logout(false);
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="e"></param>
      public void Login(bool autoLogin)
      {
         try
         {
            string userName, password, loginServer;
            DialogResult result = UserDetails.GetUserId(gui_.hostForm, "Enter login details",
               Program.AppConfig, autoLogin, out userName, out password, out loginServer);

            switch (result)
            {
               case DialogResult.OK:      // User entered an explicit setup
               case DialogResult.Yes:     // User wants to keep the same login as before
                  break;

               case DialogResult.Abort:   // User aborted entering new details
                  logger_.DebugFormat("cancelling login ({0})", result.ToString());
                  return;

               case DialogResult.Cancel:  // User hit Cancel when asked if to use the same settings
                  Logout(false);
                  return;
            }

            logger_.DebugFormat("logging in as '{0}'", userName);
            account_ = Program.Loader.Create<IAccount>();
            if (!account_.Initialize(userName, password, loginServer))
            {
               // Clean up
               logger_.WarnFormat("failed to initialize account '{0}'", userName);
               account_.Shutdown();
               return;
            }

            // Ask the transport layer to login on the account
            Program.Transport.Connect(account_);

            SetStatus(HeartbeatStatus.Active, "logged in");
            gui_.statusText.Text = statusText_;

            logger_.DebugFormat("logging in as '{0}'", userName);
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         finally
         {
            gui_.menuLogin.Enabled = (null == account_);
            gui_.menuLogout.Enabled = (null != account_);
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      public void Logout(bool shuttingDown)
      {
         try
         {
            if (null != account_)
            {
               SetStatus(HeartbeatStatus.Inactive, "logged out");

               // Tell the world that we are logging out
               Program.Transport.Disconnect(account_);
               account_.Shutdown();
               account_ = null;
            }

            if (!shuttingDown)
            {
               gui_.statusText.Text = "Need to log in via File>Login";
            }
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         finally
         {
            gui_.menuLogin.Enabled = (null == account_);
            gui_.menuLogout.Enabled = (null != account_);
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Process network state change
      /// </summary>
      /// <param name="sender"></param>
      /// <param name="eventArgs"></param>
      void HandleStateChanged(object sender, NetworkStateChangedEventArgs eventArgs)
      {
         try
         {
            DateTime now = DateTime.Now;

            // Update the connection status
            switch (eventArgs.state)
            {
               case NetworkStateConstants.Idle:
                  transportConnected_ = false;
                  break;

               case NetworkStateConstants.Connected:
                  transportConnected_ = true;
                  break;

               default:
                  logger_.ErrorFormat("unknown/unexpected network state {0}", eventArgs.state);
                  transportConnected_ = false;
                  break;
            }

            // Update the GUI
            SafeAsyncInvoker.BeginInvoke(gui_.statusBar, () =>
            {
               switch (eventArgs.state)
               {
                  case NetworkStateConstants.Idle:
                     gui_.statusNetwork.Text = "network: down";
                     gui_.statusNetwork.BackColor = Color.Red;
                     break;

                  case NetworkStateConstants.Connected:
                     gui_.statusNetwork.Text = "network: up";
                     gui_.statusNetwork.BackColor = SystemColors.Control;
                     break;

                  default:
                     logger_.ErrorFormat("unknown/unexpected network state {0}", eventArgs.state);
                     break;
               }
            });
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      public ITransport Transport
      {
         get { return Program.Transport; }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      public int HeartbeatSeconds
      {
         get { return Program.Settings.HeartbeatSeconds; }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      public HeartbeatStatus Status
      {
         get { return status_; }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      public string StatusText
      {
         get { return statusText_; }
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="status"></param>
      /// <param name="statusText"></param>
      public void SetStatus(HeartbeatStatus status, string statusText)
      {
         status_ = status;
         statusText_ = statusText;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <returns></returns>
      public string GetDisplayName(string senderId)
      {
         try
         {
            HeartbeatSender sender;
            if (senders_.TryGetValue(senderId, out sender))
            {
               return sender.DisplayName;
            }
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return null;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      public bool LoggedIn
      {
         get { return (null != account_) && !string.IsNullOrEmpty(account_.SessionId); }
      }
   }
}
